package com.googlecode.perfectable.eventable.sourcing.memory;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import java.util.Deque;
import java.util.LinkedList;
import java.util.List;

import com.googlecode.perfectable.eventable.sourcing.Chronology;
import com.googlecode.perfectable.eventable.sourcing.Operation;
import com.googlecode.perfectable.eventable.sourcing.Transaction;
import com.googlecode.perfectable.eventable.sourcing.Version;
import com.googlecode.perfectable.eventable.sourcing.Transaction.Rollback;

public class MemoryChronology implements Chronology {

	private Version latest = new InitialVersion();

	public static MemoryChronology create() {
		return new MemoryChronology();
	}

	@Override
	public void perform(Operation operation) throws Rollback {
		operation.perform();
		RegularVersion newVersion = new RegularVersion(operation, this.latest());
		this.latest = newVersion;
	}

	@Override
	public Transaction begin() {
		return new MemoryTransaction(this.latest());
	}

	private MemoryChronology() {
	}

	@Override
	public Version latest() {
		return this.latest;
	}

	private class MemoryTransaction implements Transaction {
		private final List<Operation> operations = new LinkedList<>();
		private boolean closed = false;
		private final Version initial;

		public MemoryTransaction(Version initial) {
			this.initial = initial;
		}

		@Override
		public void schedule(Operation mutator) {
			this.operations.add(mutator);
		}

		@Override
		public void commit() throws Rollback {
			checkState(!this.closed);
			Deque<Version> intermediate = new LinkedList<>();
			Version current = MemoryChronology.this.latest();
			while(this.initial != current) {
				intermediate.addLast(current);
				current = current.previous();
			}
			for(Operation operation : this.operations) {
				Operation actual = checkNotNull(operation);
				for(Version version : intermediate) {
					actual = version.operation().merge(actual);
				}
				MemoryChronology.this.perform(actual);
			}
			this.closed = true;
		}

	}

	public static class InitialVersion implements Version {
		@Override
		public boolean after(Version other) {
			return false;
		}

		@Override
		public Version previous() {
			throw new IllegalStateException();
		}

		@Override
		public Operation operation() {
			throw new IllegalStateException();
		}

	}

	public static class RegularVersion implements Version {
		private Version previous;
		private Operation operation;

		public RegularVersion(Operation operation, Version previous) {
			this.operation = operation;
			this.previous = previous;
		}

		@Override
		public boolean after(Version other) {
			if(this.previous.equals(other)) {
				return true;
			}
			return this.previous.after(other);
		}

		@Override
		public Version previous() {
			return this.previous;
		}

		@Override
		public Operation operation() {
			return this.operation;
		}
	}

}
